

/*-------------------------------------------------------------------------
 *
 * bufpage.c
 *	  POSTGRES 标准缓冲区页面代码。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/storage/page/bufpage.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/htup_details.h"
#include "access/itup.h"
#include "access/xlog.h"
#include "pgstat.h"
#include "storage/checksum.h"
#include "utils/memdebug.h"
#include "utils/memutils.h"


/* GUC 变量 */
bool		ignore_checksum_failure = false;


/*******************************
 * ----------------------------------------------------------------
 *						页面支持函数
 * ----------------------------------------------------------------
 ********************************/

/*******************************
 * PageInit
 *		初始化页面内容。
 *		注意，我们在这里不计算初始校验和；那是在写入时
 *		进行的。
 ********************************/
void PageInit(Page fc_page, Size fc_pageSize, Size fc_specialSize)
{
	PageHeader	fc_p = (PageHeader) fc_page;

	fc_specialSize = MAXALIGN(fc_specialSize);

	Assert(fc_pageSize == BLCKSZ);
	Assert(fc_pageSize > fc_specialSize + SizeOfPageHeaderData);

	/*******************************
 * 确保页面的所有字段都为零，以及未使用的空间
 ********************************/
	MemSet(fc_p, 0, fc_pageSize);

	fc_p->pd_flags = 0;
	fc_p->pd_lower = SizeOfPageHeaderData;
	fc_p->pd_upper = fc_pageSize - fc_specialSize;
	fc_p->pd_special = fc_pageSize - fc_specialSize;
	PageSetPageSizeAndVersion(fc_page, fc_pageSize, PG_PAGE_LAYOUT_VERSION);
	/*******************************
 * p->pd_prune_xid = InvalidTransactionId;		由上述 MemSet 完成
 ********************************/
}


/*******************************
 * PageIsVerifiedExtended
 *		检查页面头和校验和（如果有的话）是否有效。
 *
 * 这在从磁盘刚读入页面时调用。其目的是
 * 便宜地检测损坏的页面，避免在跟踪虚假行
 * 指针、测试无效事务标识符等方面过于复杂。
 *
 * 实际上也有必要允许零填充的页面。虽然此例程在
 * 故意将页面添加到关系时 *不会* 被调用，
 * 但在某些情况下，可能会在表中找到零填充的页面。
 * （例如：后端扩展关系，然后在可以写入
 * 新页面的任何 WAL 条目之前崩溃。内核将已经在
 * 文件中拥有零填充页面，并且在重启后仍将保持这种状态。）
 * 因此，我们在这里允许零填充页面，并小心确保页面
 * 访问宏将这样的页面视为空且没有空闲空间。
 * 最终，VACUUM 将清理这样的页面并使其可用。
 *
 * 如果设置了标志 PIV_LOG_WARNING，则在校验和失败时记录
 * 警告。
 *
 * 如果设置了标志 PIV_REPORT_STAT，则将校验和失败直接报告
 * 给 pgstat。
 ********************************/
bool PageIsVerifiedExtended(Page fc_page, BlockNumber fc_blkno, int fc_flags)
{
	PageHeader	fc_p = (PageHeader) fc_page;
	size_t	   *fc_pagebytes;
	int			fc_i;
	bool		fc_checksum_failure = false;
	bool		fc_header_sane = false;
	bool		fc_all_zeroes = false;
	uint16		fc_checksum = 0;

	/*******************************
	 * 除非页面通过基本非零测试，否则不要验证页面数据
 ********************************/
	if (!PageIsNew(fc_page))
	{
		if (DataChecksumsEnabled())
		{
			fc_checksum = pg_checksum_page((char *) fc_page, fc_blkno);

			if (fc_checksum != fc_p->pd_checksum)
				fc_checksum_failure = true;
		}

		/*******************************
		 * 以下检查无法证明头是正确的，仅证明
		 * 它看起来足够合理以允许进入缓冲池。
		 * 稍后对块的使用仍可能揭示问题，这就是我们提供
		 * 校验和选项的原因。
 ********************************/
		if ((fc_p->pd_flags & ~PD_VALID_FLAG_BITS) == 0 &&
			fc_p->pd_lower <= fc_p->pd_upper &&
			fc_p->pd_upper <= fc_p->pd_special &&
			fc_p->pd_special <= BLCKSZ &&
			fc_p->pd_special == MAXALIGN(fc_p->pd_special))
			fc_header_sane = true;

		if (fc_header_sane && !fc_checksum_failure)
			return true;
	}

	/*******************************
 * 检查全零情况
 ********************************/
	fc_all_zeroes = true;
	fc_pagebytes = (size_t *) fc_page;
	for (fc_i = 0; fc_i < (BLCKSZ / sizeof(size_t)); fc_i++)
	{
		if (fc_pagebytes[fc_i] != 0)
		{
			fc_all_zeroes = false;
			break;
		}
	}

	if (fc_all_zeroes)
		return true;

	/*
	 * 如果校验和失败，则抛出警告，但仅在检查过全零的情况后才执行。
	 */
	if (fc_checksum_failure)
	{
		if ((fc_flags & PIV_LOG_WARNING) != 0)
			ereport(WARNING,
					(errcode(ERRCODE_DATA_CORRUPTED),
					 errmsg("page verification failed, calculated checksum %u but expected %u",
							fc_checksum, fc_p->pd_checksum)));

		if ((fc_flags & PIV_REPORT_STAT) != 0)
			pgstat_report_checksum_failure();

		if (fc_header_sane && ignore_checksum_failure)
			return true;
	}

	return false;
}



/*
 *	PageAddItemExtended
 *
 *	向页面添加项目。返回值是插入的位置偏移，如果由于任何原因未插入该项目，则返回 InvalidOffsetNumber。会发出警告，指示拒绝的原因。
 *
 *	offsetNumber 必须是 InvalidOffsetNumber，以指定寻找一个空闲行指针，或者在 FirstOffsetNumber 和最后一个现有项目之后的一个值，以指定使用那个特定的行指针。
 *
 *	如果 offsetNumber 是有效的，并且标志 PAI_OVERWRITE 被设置，我们只需在指定的 offsetNumber 存储项目，该 offsetNumber 必须是当前未使用的行指针，或是在最后一个现有项目之后的一个。
 *
 *	如果 offsetNumber 是有效的并且标志 PAI_OVERWRITE 没有被设置，则在指定的 offsetNumber 插入项目，移动数组中现有的项目以腾出空间。
 *
 *	如果 offsetNumber 是无效的，则通过寻找第一个未使用且已解除分配的槽来分配一个槽。
 *
 *	如果标志 PAI_IS_HEAP 被设置，我们强制确保页面上不能有超过 MaxHeapTuplesPerPage 个行指针。
 *
 *	!!! 此处不允许 EREPORT(ERROR) !!!
 */
OffsetNumber PageAddItemExtended(Page fc_page,
					Item fc_item,
					Size fc_size,
					OffsetNumber fc_offsetNumber,
					int fc_flags)
{
	PageHeader	fc_phdr = (PageHeader) fc_page;
	Size		fc_alignedSize;
	int			fc_lower;
	int			fc_upper;
	ItemId		fc_itemId;
	OffsetNumber fc_limit;
	bool		fc_needshuffle = false;

	/*
	 * 对损坏的页面指针要小心
	 */
	if (fc_phdr->pd_lower < SizeOfPageHeaderData ||
		fc_phdr->pd_lower > fc_phdr->pd_upper ||
		fc_phdr->pd_upper > fc_phdr->pd_special ||
		fc_phdr->pd_special > BLCKSZ)
		ereport(PANIC,
				(errcode(ERRCODE_DATA_CORRUPTED),
				 errmsg("corrupted page pointers: lower = %u, upper = %u, special = %u",
						fc_phdr->pd_lower, fc_phdr->pd_upper, fc_phdr->pd_special)));

	/*
	 * 选择 offsetNumber 以放置新项目
	 */
	fc_limit = OffsetNumberNext(PageGetMaxOffsetNumber(fc_page));

	/* 是否传入了 offsetNumber? */
	if (OffsetNumberIsValid(fc_offsetNumber))
	{
		/* 是的，检查它 */
		if ((fc_flags & PAI_OVERWRITE) != 0)
		{
			if (fc_offsetNumber < fc_limit)
			{
				fc_itemId = PageGetItemId(fc_phdr, fc_offsetNumber);
				if (ItemIdIsUsed(fc_itemId) || ItemIdHasStorage(fc_itemId))
				{
					elog(WARNING, "will not overwrite a used ItemId");
					return InvalidOffsetNumber;
				}
			}
		}
		else
		{
			if (fc_offsetNumber < fc_limit)
				fc_needshuffle = true; /* 需要移动现有的行指针 */
		}
	}
	else
	{
		/* offsetNumber没有被传递，因此需要找到一个空闲槽 */
		/* 如果没有空闲槽，我们将放置在限制位置（第一个开放槽） */
		if (PageHasFreeLinePointers(fc_phdr))
		{
			/*
			 * 扫描行指针数组以定位一个“可回收的”（未使用的）
			 * ItemId。
			 *
			 * 始终优先使用较早的项目。 PageTruncateLinePointerArray
			 * 只能缩短未使用的项目，当它们出现在行指针数组的末尾作为一个连续的
			 * 组时。
			 */
			for (fc_offsetNumber = FirstOffsetNumber;
				 fc_offsetNumber < fc_limit;	/* limit是maxoff+1 */
				 fc_offsetNumber++)
			{
				fc_itemId = PageGetItemId(fc_phdr, fc_offsetNumber);

				/*
				 * 我们还检查没有存储，以防万一；
				 * 未使用的项目永远不应有存储。 Assert()以确保
				 * 不变性也被遵守。
				 */
				Assert(ItemIdIsUsed(fc_itemId) || !ItemIdHasStorage(fc_itemId));

				if (!ItemIdIsUsed(fc_itemId) && !ItemIdHasStorage(fc_itemId))
					break;
			}
			if (fc_offsetNumber >= fc_limit)
			{
				/* 提示是错误的，因此重置它 */
				PageClearHasFreeLinePointers(fc_phdr);
			}
		}
		else
		{
			/* 如果提示说没有空闲槽，就不要再搜索了 */
			fc_offsetNumber = fc_limit;
		}
	}

	/* 拒绝在第一个未使用的行指针之后放置项目 */
	if (fc_offsetNumber > fc_limit)
	{
		elog(WARNING, "specified item offset is too large");
		return InvalidOffsetNumber;
	}

	/* 拒绝在堆边界之外放置项目，如果有堆的话 */
	if ((fc_flags & PAI_IS_HEAP) != 0 && fc_offsetNumber > MaxHeapTuplesPerPage)
	{
		elog(WARNING, "can't put more than MaxHeapTuplesPerPage items in a heap page");
		return InvalidOffsetNumber;
	}

	/*
	 * 计算页面的新下指针和上指针，查看是否能够适应。
	 *
	 * 注意：进行算术运算时使用有符号整数，以避免例如
	 * alignedSize > pd_upper 这种情况导致的错误。
	 */
	if (fc_offsetNumber == fc_limit || fc_needshuffle)
		fc_lower = fc_phdr->pd_lower + sizeof(ItemIdData);
	else
		fc_lower = fc_phdr->pd_lower;

	fc_alignedSize = MAXALIGN(fc_size);

	fc_upper = (int) fc_phdr->pd_upper - (int) fc_alignedSize;

	if (fc_lower > fc_upper)
		return InvalidOffsetNumber;

	/*
	 * 可以插入项目。首先，如果需要，重新排列现有指针。
	 */
	fc_itemId = PageGetItemId(fc_phdr, fc_offsetNumber);

	if (fc_needshuffle)
		memmove(fc_itemId + 1, fc_itemId,
				(fc_limit - fc_offsetNumber) * sizeof(ItemIdData));

	/* 设置行指针 */
	ItemIdSetNormal(fc_itemId, fc_upper, fc_size);

	/*
	 * 项目通常不包含未初始化的字节。核心 bufpage 使用者
	 * 遵循此规范，但这不是强制的编码规则；一个新的索引 AM
	 * 可以选择偏离这条规则。然而，数据类型输入函数和其他
	 * C 语言函数在合成数据时应该初始化所有字节；datumIsEqual()
	 * 依赖于此。在这里进行测试，以及在 printtup() 中的类似检查，有助于捕捉此类错误。
	 *
	 * 通过仅索引扫描检索的“name”类型的值可能包含
	 * 未初始化的字节；请参见 btrescan() 中的注释。Valgrind 将报告
	 * 这是一个错误，但可以安全地忽略。
	 */
	VALGRIND_CHECK_MEM_IS_DEFINED(item, size);

	/* 将项目的数据复制到页面上 */
	memcpy((char *) fc_page + fc_upper, fc_item, fc_size);

	/* 调整页面头部 */
	fc_phdr->pd_lower = (LocationIndex) fc_lower;
	fc_phdr->pd_upper = (LocationIndex) fc_upper;

	return fc_offsetNumber;
}


/*
 * PageGetTempPage
 *		在本地内存中获取一个临时页面以进行特殊处理。
 *		返回的页面完全未初始化；调用者必须进行初始化。
 */
Page PageGetTempPage(Page fc_page)
{
	Size		fc_pageSize;
	Page		fc_temp;

	fc_pageSize = PageGetPageSize(fc_page);
	fc_temp = (Page) palloc(fc_pageSize);

	return fc_temp;
}

/*
 * PageGetTempPageCopy
 *		在本地内存中获取一个临时页面以进行特殊处理。
 *		该页面通过复制给定页面的内容进行初始化。
 */
Page PageGetTempPageCopy(Page fc_page)
{
	Size		fc_pageSize;
	Page		fc_temp;

	fc_pageSize = PageGetPageSize(fc_page);
	fc_temp = (Page) palloc(fc_pageSize);

	memcpy(fc_temp, fc_page, fc_pageSize);

	return fc_temp;
}

/*
 * PageGetTempPageCopySpecial
 *		在本地内存中获取一个临时页面以进行特殊处理。
 *		该页面用与给定页面相同的特殊空间大小进行 PageInit，并复制
 *		给定页面中的特殊空间。
 */
Page PageGetTempPageCopySpecial(Page fc_page)
{
	Size		fc_pageSize;
	Page		fc_temp;

	fc_pageSize = PageGetPageSize(fc_page);
	fc_temp = (Page) palloc(fc_pageSize);

	PageInit(fc_temp, fc_pageSize, PageGetSpecialSize(fc_page));
	memcpy(PageGetSpecialPointer(fc_temp),
		   PageGetSpecialPointer(fc_page),
		   PageGetSpecialSize(fc_page));

	return fc_temp;
}

/*
 * PageRestoreTempPage
 *		在特殊处理后将临时页面复制回永久页面
 *		并释放临时页面。
 */
void PageRestoreTempPage(Page fc_tempPage, Page fc_oldPage)
{
	Size		fc_pageSize;

	fc_pageSize = PageGetPageSize(fc_tempPage);
	memcpy((char *) fc_oldPage, (char *) fc_tempPage, fc_pageSize);

	pfree(fc_tempPage);
}

/*
 * Tuple defrag 支持 PageRepairFragmentation 和 PageIndexMultiDelete
 */
typedef struct itemIdCompactData
{
	uint16		offsetindex;	/* linp 数组索引 */
	int16		itemoff;		/* 项目数据的页面偏移量 */
	uint16		alignedlen;		/* MAXALIGN(项目数据长度) */
} itemIdCompactData;
typedef itemIdCompactData *itemIdCompact;

/*
 * 在移除或标记一些行指针为未使用后，移动元组以
 * 消除因移除项目而造成的间隙，并将它们重新排序回
 * 页面中的反向行指针顺序。
 *
 * 此函数通常会非常热门，因此采取一些措施以使其尽可能
 * 最优是有必要的。
 *
 * 调用者可以将 'presorted' 作为 true 传递，如果 'itemidbase'
 * 数组按 itemoff 的降序排序。当这是 true 时，我们可以
 * 直接使用 memmove() 将元组移动到页面的末尾。这是一个相当
 * 常见的情况，因为这是元组最初插入页面的顺序。当我们调用
 * 此函数以对页面中的元组进行碎片整理时，任何添加到页面的新行
 * 指针将保持该预排序顺序，因此在经常更新的表中碰到此情况
 * 的可能性仍然很高。
 *
 * 当 'itemidbase' 数组没有预排序时，我们无法自由地
 * 使用 memmove() 移动元组。这样做可能会导致我们覆盖
 * 尚未移动的元组所属的内存。在这种情况下，我们将所有
 * 需要移动的元组复制到一个临时缓冲区中。然后，我们可以
 * 直接从该临时缓冲区使用 memcpy() 将内容复制回页面中的
 * 正确位置。元组按 'itemidbase' 数组中的顺序复制回
 * 页面，因此我们最终会将元组重新排序回反向行指针顺序。
 * 这将增加下次触发预排序情况的可能性。
 *
 * 调用者必须确保 nitems > 0
 */
static void fc_compactify_tuples(itemIdCompact fc_itemidbase, int fc_nitems, Page fc_page, bool fc_presorted)
{
	PageHeader	fc_phdr = (PageHeader) fc_page;
	Offset		fc_upper;
	Offset		fc_copy_tail;
	Offset		fc_copy_head;
	itemIdCompact fc_itemidptr;
	int			fc_i;

	/* 如果 nitems == 0，内部代码将无法正常工作 */
	Assert(fc_nitems > 0);

	if (fc_presorted)
	{

#ifdef USE_ASSERT_CHECKING
		{
			/*
			 * 验证我们没有得到任何错误的调用者
			 * 传递了一个真正的预排序值。
			 */
			Offset		fc_lastoff = fc_phdr->pd_special;

			for (fc_i = 0; fc_i < fc_nitems; fc_i++)
			{
				fc_itemidptr = &fc_itemidbase[fc_i];

				Assert(fc_lastoff > fc_itemidptr->itemoff);

				fc_lastoff = fc_itemidptr->itemoff;
			}
		}
#endif							/* USE_ASSERT_CHECKING */

		/*
		 * 'itemidbase' 已经处于最佳顺序，即，较低项
		 * 指针具有较高的偏移量。这使我们可以
		 * memmove() 元组到页面的末尾，而不必担心
		 * 覆盖尚未移动的其他元组。
		 *
		 * 很有可能，页面的末尾已经有元组，我们可以简单
		 * 跳过它们，因为它们已经位于页面中的正确位置。
		 * 我们将首先做这一点……
		 */
		fc_upper = fc_phdr->pd_special;
		fc_i = 0;
		do
		{
			fc_itemidptr = &fc_itemidbase[fc_i];
			if (fc_upper != fc_itemidptr->itemoff + fc_itemidptr->alignedlen)
				break;
			fc_upper -= fc_itemidptr->alignedlen;

			fc_i++;
		} while (fc_i < fc_nitems);

		/*
		 * 既然我们找到了第一个需要移动的元组，我们就可以
		 * 进行元组压缩。我们尽量减少 memmove() 调用的次数，
		 * 只有在有间隙时才调用 memmove()。当我们看到有间隙时
		 * 我们只需移动所有在间隙之后的元组，直到最后一次
		 * 移动操作的位置。
		 */
		fc_copy_tail = fc_copy_head = fc_itemidptr->itemoff + fc_itemidptr->alignedlen;
		for (; fc_i < fc_nitems; fc_i++)
		{
			ItemId		fc_lp;

			fc_itemidptr = &fc_itemidbase[fc_i];
			fc_lp = PageGetItemId(fc_page, fc_itemidptr->offsetindex + 1);

			if (fc_copy_head != fc_itemidptr->itemoff + fc_itemidptr->alignedlen)
			{
				memmove((char *) fc_page + fc_upper,
						fc_page + fc_copy_head,
						fc_copy_tail - fc_copy_head);

				
/*
				 * 我们现在已经移动了所有已经看到的元组，但不包括当前元组，因此我们将 copy_tail 设置为该元组的末尾，以便在循环的另一个迭代中进行移动。
				 */
				fc_copy_tail = fc_itemidptr->itemoff + fc_itemidptr->alignedlen;
			}
			/* 将目标偏移量向下移动该元组的长度 */
			fc_upper -= fc_itemidptr->alignedlen;
			/* 将 copy_head 指向该元组的起始位置 */
			fc_copy_head = fc_itemidptr->itemoff;

			/* 更新行指针以引用新的偏移量 */
			fc_lp->lp_off = fc_upper;
		}

		/* 移动剩余的元组。 */
		memmove((char *) fc_page + fc_upper,
				fc_page + fc_copy_head,
				fc_copy_tail - fc_copy_head);
	}
	else
	{
		PGAlignedBlock fc_scratch;
		char	   *fc_scratchptr = fc_scratch.data;

		/*
		 * 非预排序情况：itemidbase 数组中的元组可能是任意顺序。因此，为了将这些元组移动到页面的末尾，我们必须在将它们复制回新的偏移量之前，先制作每个需要移动元组的临时副本。
		 *
		 * 如果被修剪的元组占比较大（>75%），我们将逐个元组地将其复制到临时缓冲区，否则，我们将为所有需要移动的元组执行一次 memcpy() 操作。
		 * 当如此多的元组被移除时，可能会有很多空隙，且不太可能在页面末尾仍然有许多不可移动的元组。
		 */
		if (fc_nitems < PageGetMaxOffsetNumber(fc_page) / 4)
		{
			fc_i = 0;
			do
			{
				fc_itemidptr = &fc_itemidbase[fc_i];
				memcpy(fc_scratchptr + fc_itemidptr->itemoff, fc_page + fc_itemidptr->itemoff,
					   fc_itemidptr->alignedlen);
				fc_i++;
			} while (fc_i < fc_nitems);

			/* 为下面的紧凑化代码设置准备 */
			fc_i = 0;
			fc_itemidptr = &fc_itemidbase[0];
			fc_upper = fc_phdr->pd_special;
		}
		else
		{
			fc_upper = fc_phdr->pd_special;

			/*
			 * 许多元组可能已经在正确的位置上。
			 * 没必要将它们复制到临时缓冲区。相反，我们将直接在 itemidbase 数组中向前跳到需要移动元组的位置，以便下面的代码只需不去动这些元组。
			 */
			fc_i = 0;
			do
			{
				fc_itemidptr = &fc_itemidbase[fc_i];
				if (fc_upper != fc_itemidptr->itemoff + fc_itemidptr->alignedlen)
					break;
				fc_upper -= fc_itemidptr->alignedlen;

				fc_i++;
			} while (fc_i < fc_nitems);

			/* 复制所有需要移动到临时缓冲区的元组 */
			memcpy(fc_scratchptr + fc_phdr->pd_upper,
				   fc_page + fc_phdr->pd_upper,
				   fc_upper - fc_phdr->pd_upper);
		}

		/*
		 * 执行元组压缩。itemidptr 已经指向我们将要移动的第一个元组。
		 * 在这里，我们将相邻元组的 memcpy 调用合并为一个调用。
		 * 这是通过延迟 memcpy 调用，直到我们发现需要关闭的间隙来实现的。
		 */
		fc_copy_tail = fc_copy_head = fc_itemidptr->itemoff + fc_itemidptr->alignedlen;
		for (; fc_i < fc_nitems; fc_i++)
		{
			ItemId		fc_lp;

			fc_itemidptr = &fc_itemidbase[fc_i];
			fc_lp = PageGetItemId(fc_page, fc_itemidptr->offsetindex + 1);

			/* 当我们检测到间隙时复制待处理的元组 */
			if (fc_copy_head != fc_itemidptr->itemoff + fc_itemidptr->alignedlen)
			{
				memcpy((char *) fc_page + fc_upper,
					   fc_scratchptr + fc_copy_head,
					   fc_copy_tail - fc_copy_head);

				/*
				 * 我们现在已经复制了所有已经看到的元组，但还没有复制
				 * 当前的元组，因此我们将 copy_tail 设置为该元组的末尾。
				 */
				fc_copy_tail = fc_itemidptr->itemoff + fc_itemidptr->alignedlen;
			}
			/* 将目标偏移量向下移动该元组的长度 */
			fc_upper -= fc_itemidptr->alignedlen;
			/* 将 copy_head 指向该元组的起始位置 */
			fc_copy_head = fc_itemidptr->itemoff;

			/* 更新行指针以引用新的偏移量 */
			fc_lp->lp_off = fc_upper;
		}

		/* 复制剩余的块 */
		memcpy((char *) fc_page + fc_upper,
			   fc_scratchptr + fc_copy_head,
			   fc_copy_tail - fc_copy_head);
	}

	fc_phdr->pd_upper = fc_upper;
}

/*
 * PageRepairFragmentation
 *
 * 在修剪后释放堆页面上的碎片空间。
 *
 * 此例程仅可用于堆页面，但请参见 PageIndexMultiDelete。
 *
 * 此例程从行指针数组的末尾移除未使用的行指针。
 * 当死的堆专用元组通过修剪被移除时，这是可能的，
 * 特别是当之前有多个元组的 HOT 链时。
 *
 * 调用者最好在页面的缓冲区上拥有完整的清理锁。
 * 作为副作用，页面的 PD_HAS_FREE_LINES 提示位将根据需要设置或清除。
 * 调用者还可能需要考虑由于数组截断而导致的行指针数组长度减少。
 */
void PageRepairFragmentation(Page fc_page)
{
	Offset		fc_pd_lower = ((PageHeader) fc_page)->pd_lower;
	Offset		fc_pd_upper = ((PageHeader) fc_page)->pd_upper;
	Offset		fc_pd_special = ((PageHeader) fc_page)->pd_special;
	Offset		fc_last_offset;
	itemIdCompactData fc_itemidbase[MaxHeapTuplesPerPage];
	itemIdCompact fc_itemidptr;
	ItemId		fc_lp;
	int			fc_nline,
				fc_nstorage,
				fc_nunused;
	OffsetNumber fc_finalusedlp = InvalidOffsetNumber;
	int			fc_i;
	Size		fc_totallen;
	bool		fc_presorted = true;	/* For now */

	/*
	 * 在这里比大多数地方更加小心是值得的，
	 * 因为我们即将重新排列（通常是）共享
	 * 磁盘缓冲区中的数据。如果我们不小心，
	 * 损坏的指针、长度等可能导致我们覆盖相邻的磁盘缓冲区，
	 * 进一步传播数据丢失。因此，检查一切。
	 */
	if (fc_pd_lower < SizeOfPageHeaderData ||
		fc_pd_lower > fc_pd_upper ||
		fc_pd_upper > fc_pd_special ||
		fc_pd_special > BLCKSZ ||
		fc_pd_special != MAXALIGN(fc_pd_special))
		ereport(ERROR,
				(errcode(ERRCODE_DATA_CORRUPTED),
				 errmsg("corrupted page pointers: lower = %u, upper = %u, special = %u",
						fc_pd_lower, fc_pd_upper, fc_pd_special)));

	/*
	 * 遍历行指针数组并收集有关活动项目的数据。
	 */
	fc_nline = PageGetMaxOffsetNumber(fc_page);
	fc_itemidptr = fc_itemidbase;
	fc_nunused = fc_totallen = 0;
	fc_last_offset = fc_pd_special;
	for (fc_i = FirstOffsetNumber; fc_i <= fc_nline; fc_i++)
	{
		fc_lp = PageGetItemId(fc_page, fc_i);
		if (ItemIdIsUsed(fc_lp))
		{
			if (ItemIdHasStorage(fc_lp))
			{
				fc_itemidptr->offsetindex = fc_i - 1;
				fc_itemidptr->itemoff = ItemIdGetOffset(fc_lp);

				if (fc_last_offset > fc_itemidptr->itemoff)
					fc_last_offset = fc_itemidptr->itemoff;
				else
					fc_presorted = false;

				if (unlikely(fc_itemidptr->itemoff < (int) fc_pd_upper ||
							 fc_itemidptr->itemoff >= (int) fc_pd_special))
					ereport(ERROR,
							(errcode(ERRCODE_DATA_CORRUPTED),
							 errmsg("corrupted line pointer: %u",
									fc_itemidptr->itemoff)));
				fc_itemidptr->alignedlen = MAXALIGN(ItemIdGetLength(fc_lp));
				fc_totallen += fc_itemidptr->alignedlen;
				fc_itemidptr++;
			}

			fc_finalusedlp = fc_i;	/* 可能是最后一个非 LP_UNUSED 项目 */
		}
		else
		{
			/* 未使用的条目应具有 lp_len = 0，但确保 */
			Assert(!ItemIdHasStorage(fc_lp));
			ItemIdSetUnused(fc_lp);
			fc_nunused++;
		}
	}

	fc_nstorage = fc_itemidptr - fc_itemidbase;
	if (fc_nstorage == 0)
	{
		/* 页面完全空，因此可以快速重置 */
		((PageHeader) fc_page)->pd_upper = fc_pd_special;
	}
	else
	{
		/* 需要以困难的方式压缩页面 */
		if (fc_totallen > (Size) (fc_pd_special - fc_pd_lower))
			ereport(ERROR,
					(errcode(ERRCODE_DATA_CORRUPTED),
					 errmsg("corrupted item lengths: total %u, available space %u",
							(unsigned int) fc_totallen, fc_pd_special - fc_pd_lower)));

		fc_compactify_tuples(fc_itemidbase, fc_nstorage, fc_page, fc_presorted);
	}

	if (fc_finalusedlp != fc_nline)
	{
		/* 最后一个行指针不是最后一个使用的行指针 */
		int			fc_nunusedend = fc_nline - fc_finalusedlp;

		Assert(fc_nunused >= fc_nunusedend && fc_nunusedend > 0);

		/* 从计数中删除尾部未使用的行指针 */
		fc_nunused -= fc_nunusedend;
		/* 截断行指针数组 */
		((PageHeader) fc_page)->pd_lower -= (sizeof(ItemIdData) * fc_nunusedend);
	}

	/* 为 PageAddItemExtended 设置提示位 */
	if (fc_nunused > 0)
		PageSetHasFreeLinePointers(fc_page);
	else
		PageClearHasFreeLinePointers(fc_page);
}

/*
 * PageTruncateLinePointerArray
 *
 * 移除行指针数组末尾的未使用行指针。
 *
 * 此例程仅可用于堆页面。它由 VACUUM 在其对堆的第二次遍历中调用。
 * 我们期望页面上有至少一个 LP_UNUSED 行指针
 * （如果 VACUUM 没有在页面上将 LP_DEAD 项目设置为 LP_UNUSED，那么它就不应该在这里调用）。
 *
 * 如果需要，我们在最好保留一个剩余的 LP_UNUSED 项目，
 * 以避免将行指针数组截断为 0 项。这有点任意，
 * 但似乎是一个好主意，以避免留下一个 PageIsEmpty() 页面。
 *
 * 调用者可以在页面缓冲区上拥有独占锁或完整的清理锁。
 * 页面上的 PD_HAS_FREE_LINES 提示位将基于我们是否
 * 留下任何剩余的 LP_UNUSED 项目而设置或清除。
 */
void PageTruncateLinePointerArray(Page fc_page)
{
	PageHeader	fc_phdr = (PageHeader) fc_page;
	bool		fc_countdone = false,
				fc_sethint = false;
	int			fc_nunusedend = 0;

	/* 从后往前扫描行指针数组 */
	for (int fc_i = PageGetMaxOffsetNumber(fc_page); fc_i >= FirstOffsetNumber; fc_i--)
	{
		ItemId		fc_lp = PageGetItemId(fc_page, fc_i);

		if (!fc_countdone && fc_i > FirstOffsetNumber)
		{
			/*
			 * 仍在确定数组末尾的哪些行指针将被截断。
			 * 要么计算另一个行指针可以安全截断，要么注意
			 * 额外的行指针不安全的截断（停止计算行指针）。
			 */
			if (!ItemIdIsUsed(fc_lp))
				fc_nunusedend++;
			else
				fc_countdone = true;
		}
		else
		{
			/*
			 * 一旦我们停止计数，我们仍然需要弄清楚是否
			 * 在数组的前面还有任何剩余的 LP_UNUSED 行指针。
			 */
			if (!ItemIdIsUsed(fc_lp))
			{
				/*
				 * 这是一个未使用的行指针，我们不会截断
				 * -- 所以至少有一个。设置页面的提示。
				 */
				fc_sethint = true;
				break;
			}
		}
	}

	if (fc_nunusedend > 0)
	{
		fc_phdr->pd_lower -= sizeof(ItemIdData) * fc_nunusedend;

#ifdef CLOBBER_FREED_MEMORY
		memset((char *) fc_page + fc_phdr->pd_lower, 0x7F,
			   sizeof(ItemIdData) * fc_nunusedend);
#endif
	}
	else
		Assert(fc_sethint);

	/* 为 PageAddItemExtended 设置提示位 */
	if (fc_sethint)
		PageSetHasFreeLinePointers(fc_page);
	else
		PageClearHasFreeLinePointers(fc_page);
}

/*
 * PageGetFreeSpace
 *		返回页面上可用（可分配）空间的大小，
 *		减少新行指针所需的空间。
 *
 * 注意：这通常只能用于索引页面。请在堆页面上使用
 * PageGetHeapFreeSpace。
 */
Size PageGetFreeSpace(Page fc_page)
{
	int			fc_space;

	/*
	 * 在这里使用有符号算术，以便当 pd_lower >
	 * pd_upper 时能够合理地处理。
	 */
	fc_space = (int) ((PageHeader) fc_page)->pd_upper -
		(int) ((PageHeader) fc_page)->pd_lower;

	if (fc_space < (int) sizeof(ItemIdData))
		return 0;
	fc_space -= sizeof(ItemIdData);

	return (Size) fc_space;
}

/*
 * PageGetFreeSpaceForMultipleTuples
 *		返回页面上可用（可分配）空间的大小，
 *		减少多个新行指针所需的空间。
 *
 * 注意：这通常只能用于索引页面。请在堆页面上使用
 * PageGetHeapFreeSpace。
 */
Size PageGetFreeSpaceForMultipleTuples(Page fc_page, int fc_ntups)
{
	int			fc_space;

	/*
	 * 在这里使用有符号算术，以便当 pd_lower >
	 * pd_upper 时能够合理地处理。
	 */
	fc_space = (int) ((PageHeader) fc_page)->pd_upper -
		(int) ((PageHeader) fc_page)->pd_lower;

	if (fc_space < (int) (fc_ntups * sizeof(ItemIdData)))
		return 0;
	fc_space -= fc_ntups * sizeof(ItemIdData);

	return (Size) fc_space;
}

/*
 * PageGetExactFreeSpace
 *		返回页面上可用（可分配）空间的大小，
 *		不考虑添加/移除行指针。
 */
Size PageGetExactFreeSpace(Page fc_page)
{
	int			fc_space;

	/*
	 * 在这里使用有符号算术，以便当 pd_lower >
	 * pd_upper 时能够合理地处理。
	 */
	fc_space = (int) ((PageHeader) fc_page)->pd_upper -
		(int) ((PageHeader) fc_page)->pd_lower;

	if (fc_space < 0)
		return 0;

	return (Size) fc_space;
}



/*
 * PageGetHeapFreeSpace
 *		返回页面上可用（可分配）空间的大小，
 *		减少了新行指针所需的空间。
 *
 * 与 PageGetFreeSpace 的区别在于，如果页面中已经有 MaxHeapTuplesPerPage 个行指针，
 * 且没有空闲的，它将返回零。我们使用这个来强制在堆页面上不创建超过
 * MaxHeapTuplesPerPage 个行指针。（尽管在重定向或无效行指针的情况下，
 * 可能会有过多的行指针，但实际上不可能放入超出该数量的元组。
 * 为了避免破坏假设 MaxHeapTuplesPerPage 是行指针数量硬限制的代码，
 * 我们进行了这个额外检查。）
 */
Size PageGetHeapFreeSpace(Page fc_page)
{
	Size		fc_space;

	fc_space = PageGetFreeSpace(fc_page);
	if (fc_space > 0)
	{
		OffsetNumber fc_offnum,
					fc_nline;

		/*
		 * 页面中已经有 MaxHeapTuplesPerPage 个行指针了吗？
		 */
		fc_nline = PageGetMaxOffsetNumber(fc_page);
		if (fc_nline >= MaxHeapTuplesPerPage)
		{
			if (PageHasFreeLinePointers((PageHeader) fc_page))
			{
				/*
				 * 由于这只是一个提示，我们必须确认确实有
				 * 一个空闲的行指针
				 */
				for (fc_offnum = FirstOffsetNumber; fc_offnum <= fc_nline; fc_offnum = OffsetNumberNext(fc_offnum))
				{
					ItemId		fc_lp = PageGetItemId(fc_page, fc_offnum);

					if (!ItemIdIsUsed(fc_lp))
						break;
				}

				if (fc_offnum > fc_nline)
				{
					/*
					 * 提示是错误的，但我们不能在这里清除它，因为我们
					 * 没有能力将页面标记为脏。
					 */
					fc_space = 0;
				}
			}
			else
			{
				/*
				 * 尽管提示可能是错误的，但 PageAddItem 会相信
				 * 它，因此我们也必须相信它。
				 */
				fc_space = 0;
			}
		}
	}
	return fc_space;
}


/*
 * PageIndexTupleDelete
 *
 * 该例程用于从索引页面中删除元组。
 *
 * 与堆页面不同，我们紧凑地移除被删除元组的行指针。
 */
void PageIndexTupleDelete(Page fc_page, OffsetNumber fc_offnum)
{
	PageHeader	fc_phdr = (PageHeader) fc_page;
	char	   *fc_addr;
	ItemId		fc_tup;
	Size		fc_size;
	unsigned	fc_offset;
	int			fc_nbytes;
	int			fc_offidx;
	int			fc_nline;

	/*
	 * 与 PageRepairFragmentation 一样，过度谨慎似乎是合理的。
	 */
	if (fc_phdr->pd_lower < SizeOfPageHeaderData ||
		fc_phdr->pd_lower > fc_phdr->pd_upper ||
		fc_phdr->pd_upper > fc_phdr->pd_special ||
		fc_phdr->pd_special > BLCKSZ ||
		fc_phdr->pd_special != MAXALIGN(fc_phdr->pd_special))
		ereport(ERROR,
				(errcode(ERRCODE_DATA_CORRUPTED),
				 errmsg("corrupted page pointers: lower = %u, upper = %u, special = %u",
						fc_phdr->pd_lower, fc_phdr->pd_upper, fc_phdr->pd_special)));

	fc_nline = PageGetMaxOffsetNumber(fc_page);
	if ((int) fc_offnum <= 0 || (int) fc_offnum > fc_nline)
		elog(ERROR, "invalid index offnum: %u", fc_offnum);

	/* 将偏移量编号更改为偏移索引 */
	fc_offidx = fc_offnum - 1;

	fc_tup = PageGetItemId(fc_page, fc_offnum);
	Assert(ItemIdHasStorage(fc_tup));
	fc_size = ItemIdGetLength(fc_tup);
	fc_offset = ItemIdGetOffset(fc_tup);

	if (fc_offset < fc_phdr->pd_upper || (fc_offset + fc_size) > fc_phdr->pd_special ||
		fc_offset != MAXALIGN(fc_offset))
		ereport(ERROR,
				(errcode(ERRCODE_DATA_CORRUPTED),
				 errmsg("corrupted line pointer: offset = %u, size = %u",
						fc_offset, (unsigned int) fc_size)));

	/* 实际要删除的空间量 */
	fc_size = MAXALIGN(fc_size);

	/*
	 * 首先，我们想要去掉索引元组的 pd_linp 条目。我们将所有后续的 linp 向后复制一格。我们不使用 PageGetItemId，因为我们正在操作 _数组_，而不是单个 linp。
	 */
	fc_nbytes = fc_phdr->pd_lower -
		((char *) &fc_phdr->pd_linp[fc_offidx + 1] - (char *) fc_phdr);

	if (fc_nbytes > 0)
		memmove((char *) &(fc_phdr->pd_linp[fc_offidx]),
				(char *) &(fc_phdr->pd_linp[fc_offidx + 1]),
				fc_nbytes);

	/*
	 * 现在移动旧上限（元组空间的开始）与被删除元组的开始之间的所有内容向前，以便页面中间留出空间。如果我们刚刚删除了元组空间开始处的元组，则不需要进行复制。
	 */

	/* 元组空间的开始 */
	fc_addr = (char *) fc_page + fc_phdr->pd_upper;

	if (fc_offset > fc_phdr->pd_upper)
		memmove(fc_addr + fc_size, fc_addr, fc_offset - fc_phdr->pd_upper);

	/* 调整空闲空间边界指针 */
	fc_phdr->pd_upper += fc_size;
	fc_phdr->pd_lower -= sizeof(ItemIdData);

	/*
	 * 最后，我们需要调整剩余的 linp 条目。
	 *
	 * 在删除的元组数据之前的任何内容都向前移动了被删除元组的大小。
	 */
	if (!PageIsEmpty(fc_page))
	{
		int			fc_i;

		fc_nline--;				/* 现在比开始时少了一个 */
		for (fc_i = 1; fc_i <= fc_nline; fc_i++)
		{
			ItemId		fc_ii = PageGetItemId(fc_phdr, fc_i);

			Assert(ItemIdHasStorage(fc_ii));
			if (ItemIdGetOffset(fc_ii) <= fc_offset)
				fc_ii->lp_off += fc_size;
		}
	}
}


/*
 * PageIndexMultiDelete
 *
 * 该例程处理一次从索引页面删除多个元组的情况。它比循环调用 PageIndexTupleDelete 快得多……但是，调用者 *必须* 以项编号顺序提供要删除的项编号数组！
 */
void PageIndexMultiDelete(Page fc_page, OffsetNumber *fc_itemnos, int fc_nitems)
{
	PageHeader	fc_phdr = (PageHeader) fc_page;
	Offset		fc_pd_lower = fc_phdr->pd_lower;
	Offset		fc_pd_upper = fc_phdr->pd_upper;
	Offset		fc_pd_special = fc_phdr->pd_special;
	Offset		fc_last_offset;
	itemIdCompactData fc_itemidbase[MaxIndexTuplesPerPage];
	ItemIdData	fc_newitemids[MaxIndexTuplesPerPage];
	itemIdCompact fc_itemidptr;
	ItemId		fc_lp;
	int			fc_nline,
				fc_nused;
	Size		fc_totallen;
	Size		fc_size;
	unsigned	fc_offset;
	int			fc_nextitm;
	OffsetNumber fc_offnum;
	bool		fc_presorted = true;	/* For now */

	Assert(fc_nitems <= MaxIndexTuplesPerPage);

	/*
	 * 如果要删除的项不多，那么逐条调用 PageIndexTupleDelete 是最佳方法。以相反的顺序删除这些项，这样我们就不必考虑调整之前删除的项的编号。
	 *
	 * TODO: 调整此处的魔法数字
	 */
	if (fc_nitems <= 2)
	{
		while (--fc_nitems >= 0)
			PageIndexTupleDelete(fc_page, fc_itemnos[fc_nitems]);
		return;
	}

	/*
	 * 与 PageRepairFragmentation 一样，过度谨慎似乎是合理的。
	 */
	if (fc_pd_lower < SizeOfPageHeaderData ||
		fc_pd_lower > fc_pd_upper ||
		fc_pd_upper > fc_pd_special ||
		fc_pd_special > BLCKSZ ||
		fc_pd_special != MAXALIGN(fc_pd_special))
		ereport(ERROR,
				(errcode(ERRCODE_DATA_CORRUPTED),
				 errmsg("corrupted page pointers: lower = %u, upper = %u, special = %u",
						fc_pd_lower, fc_pd_upper, fc_pd_special)));

	/*
	 * 扫描行指针数组并构建我们要保留的项的列表。注意，我们尚未修改页面，因为我们仍在进行有效性检查。
	 */
	fc_nline = PageGetMaxOffsetNumber(fc_page);
	fc_itemidptr = fc_itemidbase;
	fc_totallen = 0;
	fc_nused = 0;
	fc_nextitm = 0;
	fc_last_offset = fc_pd_special;
	for (fc_offnum = FirstOffsetNumber; fc_offnum <= fc_nline; fc_offnum = OffsetNumberNext(fc_offnum))
	{
		fc_lp = PageGetItemId(fc_page, fc_offnum);
		Assert(ItemIdHasStorage(fc_lp));
		fc_size = ItemIdGetLength(fc_lp);
		fc_offset = ItemIdGetOffset(fc_lp);
		if (fc_offset < fc_pd_upper ||
			(fc_offset + fc_size) > fc_pd_special ||
			fc_offset != MAXALIGN(fc_offset))
			ereport(ERROR,
					(errcode(ERRCODE_DATA_CORRUPTED),
					 errmsg("corrupted line pointer: offset = %u, size = %u",
							fc_offset, (unsigned int) fc_size)));

		if (fc_nextitm < fc_nitems && fc_offnum == fc_itemnos[fc_nextitm])
		{
			/* 跳过要删除的项 */
			fc_nextitm++;
		}
		else
		{
			fc_itemidptr->offsetindex = fc_nused; /* 它将要放置的位置 */
			fc_itemidptr->itemoff = fc_offset;

			if (fc_last_offset > fc_itemidptr->itemoff)
				fc_last_offset = fc_itemidptr->itemoff;
			else
				fc_presorted = false;

			fc_itemidptr->alignedlen = MAXALIGN(fc_size);
			fc_totallen += fc_itemidptr->alignedlen;
			fc_newitemids[fc_nused] = *fc_lp;
			fc_itemidptr++;
			fc_nused++;
		}
	}

	/* 这将捕获无效或顺序错误的 itemnos[] */
	if (fc_nextitm != fc_nitems)
		elog(ERROR, "incorrect index offsets supplied");

	if (fc_totallen > (Size) (fc_pd_special - fc_pd_lower))
		ereport(ERROR,
				(errcode(ERRCODE_DATA_CORRUPTED),
				 errmsg("corrupted item lengths: total %u, available space %u",
						(unsigned int) fc_totallen, fc_pd_special - fc_pd_lower)));

	/*
	 * 看起来不错。用副本覆盖行指针，我们已经移除了所有未使用的项。
	 */
	memcpy(fc_phdr->pd_linp, fc_newitemids, fc_nused * sizeof(ItemIdData));
	fc_phdr->pd_lower = SizeOfPageHeaderData + fc_nused * sizeof(ItemIdData);

	/* 并压缩元组数据 */
	if (fc_nused > 0)
		fc_compactify_tuples(fc_itemidbase, fc_nused, fc_page, fc_presorted);
	else
		fc_phdr->pd_upper = fc_pd_special;
}


/*
 * PageIndexTupleDeleteNoCompact
 *
 * 从索引页面中移除指定的元组，但将其行指针设置为“未使用”，而不是进行压缩，只有在它是页面上的最后一个行指针时可以被移除。
 *
 * 这用于那些要求现有活跃元组的 TID 保持不变，并愿意接受未使用行指针的索引 AM。
 */
void PageIndexTupleDeleteNoCompact(Page fc_page, OffsetNumber fc_offnum)
{
	PageHeader	fc_phdr = (PageHeader) fc_page;
	char	   *fc_addr;
	ItemId		fc_tup;
	Size		fc_size;
	unsigned	fc_offset;
	int			fc_nline;

	/*
	 * 与 PageRepairFragmentation 一样，过度谨慎似乎是合理的。
	 */
	if (fc_phdr->pd_lower < SizeOfPageHeaderData ||
		fc_phdr->pd_lower > fc_phdr->pd_upper ||
		fc_phdr->pd_upper > fc_phdr->pd_special ||
		fc_phdr->pd_special > BLCKSZ ||
		fc_phdr->pd_special != MAXALIGN(fc_phdr->pd_special))
		ereport(ERROR,
				(errcode(ERRCODE_DATA_CORRUPTED),
				 errmsg("corrupted page pointers: lower = %u, upper = %u, special = %u",
						fc_phdr->pd_lower, fc_phdr->pd_upper, fc_phdr->pd_special)));

	fc_nline = PageGetMaxOffsetNumber(fc_page);
	if ((int) fc_offnum <= 0 || (int) fc_offnum > fc_nline)
		elog(ERROR, "invalid index offnum: %u", fc_offnum);

	fc_tup = PageGetItemId(fc_page, fc_offnum);
	Assert(ItemIdHasStorage(fc_tup));
	fc_size = ItemIdGetLength(fc_tup);
	fc_offset = ItemIdGetOffset(fc_tup);

	if (fc_offset < fc_phdr->pd_upper || (fc_offset + fc_size) > fc_phdr->pd_special ||
		fc_offset != MAXALIGN(fc_offset))
		ereport(ERROR,
				(errcode(ERRCODE_DATA_CORRUPTED),
				 errmsg("corrupted line pointer: offset = %u, size = %u",
						fc_offset, (unsigned int) fc_size)));

	/* 实际要删除的空间量 */
	fc_size = MAXALIGN(fc_size);

	/*
	 * 要么将行指针设置为“未使用”，要么如果它是最后一个则删除它。 （注意：下一个到最后的可能已经是未使用的，但如果是这样，我们不去麻烦尝试压缩它们。）
	 */
	if ((int) fc_offnum < fc_nline)
		ItemIdSetUnused(fc_tup);
	else
	{
		fc_phdr->pd_lower -= sizeof(ItemIdData);
		fc_nline--;				/* 现在比开始时少了一个 */
	}

	/*
	 * 现在移动旧上限（元组空间的开始）与被删除元组的开始之间的所有内容向前，以便页面中间留出空间。如果我们刚刚删除了元组空间开始处的元组，则不需要进行复制。
	 */

	/* 元组空间的开始 */
	fc_addr = (char *) fc_page + fc_phdr->pd_upper;

	if (fc_offset > fc_phdr->pd_upper)
		memmove(fc_addr + fc_size, fc_addr, fc_offset - fc_phdr->pd_upper);

	/* 调整空闲空间边界指针 */
	fc_phdr->pd_upper += fc_size;

	/*
	 * 最后，我们需要调整剩余的 linp 条目。
	 *
	 * 在删除的元组数据之前的任何内容都向前移动了被删除元组的大小。
	 */
	if (!PageIsEmpty(fc_page))
	{
		int			fc_i;

		for (fc_i = 1; fc_i <= fc_nline; fc_i++)
		{
			ItemId		fc_ii = PageGetItemId(fc_phdr, fc_i);

			if (ItemIdHasStorage(fc_ii) && ItemIdGetOffset(fc_ii) <= fc_offset)
				fc_ii->lp_off += fc_size;
		}
	}
}


/*
 * PageIndexTupleOverwrite
 *
 * 替换索引页面上指定的元组。
 *
 * 新元组被放置在旧元组原来的位置，根据需要向上或向下移动其他元组的数据，以保持页面的紧凑性。这比删除和重新插入元组更好，因为它避免了在元组大小未变化时的任何数据移动；即使在大小变化时，我们也避免了移动行指针。这可以被一个不想取消设置 LP_DEAD 位的索引 AM 使用。它也可能被一个关心元组的物理顺序以及其逻辑/ItemId 顺序的索引 AM 使用。
 *
 * 如果新元组的空间不足，返回 false。其他错误代表数据损坏问题，因此我们只是记录日志。
 */
bool PageIndexTupleOverwrite(Page fc_page, OffsetNumber fc_offnum,
						Item fc_newtup, Size fc_newsize)
{
	PageHeader	fc_phdr = (PageHeader) fc_page;
	ItemId		fc_tupid;
	int			fc_oldsize;
	unsigned	fc_offset;
	Size		fc_alignednewsize;
	int			fc_size_diff;
	int			fc_itemcount;

	/*
	 * 与 PageRepairFragmentation 一样，过度谨慎似乎是合理的。
	 */
	if (fc_phdr->pd_lower < SizeOfPageHeaderData ||
		fc_phdr->pd_lower > fc_phdr->pd_upper ||
		fc_phdr->pd_upper > fc_phdr->pd_special ||
		fc_phdr->pd_special > BLCKSZ ||
		fc_phdr->pd_special != MAXALIGN(fc_phdr->pd_special))
		ereport(ERROR,
				(errcode(ERRCODE_DATA_CORRUPTED),
				 errmsg("corrupted page pointers: lower = %u, upper = %u, special = %u",
						fc_phdr->pd_lower, fc_phdr->pd_upper, fc_phdr->pd_special)));

	fc_itemcount = PageGetMaxOffsetNumber(fc_page);
	if ((int) fc_offnum <= 0 || (int) fc_offnum > fc_itemcount)
		elog(ERROR, "invalid index offnum: %u", fc_offnum);

	fc_tupid = PageGetItemId(fc_page, fc_offnum);
	Assert(ItemIdHasStorage(fc_tupid));
	fc_oldsize = ItemIdGetLength(fc_tupid);
	fc_offset = ItemIdGetOffset(fc_tupid);

	if (fc_offset < fc_phdr->pd_upper || (fc_offset + fc_oldsize) > fc_phdr->pd_special ||
		fc_offset != MAXALIGN(fc_offset))
		ereport(ERROR,
				(errcode(ERRCODE_DATA_CORRUPTED),
				 errmsg("corrupted line pointer: offset = %u, size = %u",
						fc_offset, (unsigned int) fc_oldsize)));

	/*
	 * 确定实际的空间需求变化，检查页面溢出。
	 */
	fc_oldsize = MAXALIGN(fc_oldsize);
	fc_alignednewsize = MAXALIGN(fc_newsize);
	if (fc_alignednewsize > fc_oldsize + (fc_phdr->pd_upper - fc_phdr->pd_lower))
		return false;

	/*
	 * 重新定位现有数据并更新行指针，除非新元组与旧元组（对齐后）大小相同，在这种情况下没有事情可做。请注意，我们需要重新定位的内容是在目标元组之前的数据，而不是之后的数据，因此方便将 size_diff 表达为元组大小减少的数量，从而使其成为添加到 pd_upper 和受影响行指针的增量。
	 */
	fc_size_diff = fc_oldsize - (int) fc_alignednewsize;
	if (fc_size_diff != 0)
	{
		char	   *fc_addr = (char *) fc_page + fc_phdr->pd_upper;
		int			fc_i;

		/* 重新定位目标元组之前的所有元组数据 */
		memmove(fc_addr + fc_size_diff, fc_addr, fc_offset - fc_phdr->pd_upper);

		/* 调整空闲空间边界指针 */
		fc_phdr->pd_upper += fc_size_diff;

		/* 调整受影响的行指针 */
		for (fc_i = FirstOffsetNumber; fc_i <= fc_itemcount; fc_i++)
		{
			ItemId		fc_ii = PageGetItemId(fc_phdr, fc_i);

			/* 允许没有存储的项；目前只有 BRIN 需要这个 */
			if (ItemIdHasStorage(fc_ii) && ItemIdGetOffset(fc_ii) <= fc_offset)
				fc_ii->lp_off += fc_size_diff;
		}
	}

	/* 更新项的元组长度，而不更改其 lp_flags 字段 */
	fc_tupid->lp_off = fc_offset + fc_size_diff;
	fc_tupid->lp_len = fc_newsize;

	/* 将新元组数据复制到页面 */
	memcpy(PageGetItem(fc_page, fc_tupid), fc_newtup, fc_newsize);

	return true;
}



char * PageSetChecksumCopy(Page fc_page, BlockNumber fc_blkno)
{
	static char *fc_pageCopy = NULL;

	/* 如果我们不需要校验和，直接返回传入的数据 */
	if (PageIsNew(fc_page) || !DataChecksumsEnabled())
		return (char *) fc_page;

	/*
	 * 我们只分配一次副本空间，并在每次随后的调用中使用它。
	 * 这里使用palloc的目的是首先确保校验和代码的适当对齐，
	 * 其次是避免在从未调用此代码的进程中浪费空间。
	 */
	if (fc_pageCopy == NULL)
		fc_pageCopy = MemoryContextAlloc(TopMemoryContext, BLCKSZ);

	memcpy(fc_pageCopy, (char *) fc_page, BLCKSZ);
	((PageHeader) fc_pageCopy)->pd_checksum = pg_checksum_page(fc_pageCopy, fc_blkno);
	return fc_pageCopy;
}

/*
 * 为私有内存中的一个页面设置校验和。
 *
 * 这必须仅在我们知道没有其他进程可以修改
 * 页面缓冲区时使用。
 */
void PageSetChecksumInplace(Page fc_page, BlockNumber fc_blkno)
{
	/* 如果我们不需要校验和，直接返回 */
	if (PageIsNew(fc_page) || !DataChecksumsEnabled())
		return;

	((PageHeader) fc_page)->pd_checksum = pg_checksum_page((char *) fc_page, fc_blkno);
}
